/*
 * datatandroid - Persistence framework for Android platform
 * 
 * Copyright (C) 2010 ProjectsException
 * <http://code.google.com/p/datandroid>
 * 
 * This file is part of datatandroid.
 * 
 * datatandroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * datatandroid is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with datatandroid.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.projectsexception.datandroid.util;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class ReflectionUtil {

    private static String[] getterPreffix = { "get", "is" };
    private static final Map<String, String> numbers;
    private static final Map<String, String> primitiveTypes;

    static {
        numbers = new HashMap<String, String>(); 
        numbers.put("java.lang.Long", "long");
        numbers.put("java.lang.Integer", "int");
        numbers.put("java.lang.Short", "short");
        numbers.put("java.lang.Double", "double");
        numbers.put("java.lang.Float", "float");
        primitiveTypes = new HashMap<String, String>(); 
        primitiveTypes.putAll(numbers);
        primitiveTypes.put("java.lang.Boolean", "boolean");
        primitiveTypes.put("java.lang.Short", "short");
        primitiveTypes.put("java.lang.Character", "char");
    }
    
    public static String getWrapperPrimitive(String className) {
        if (primitiveTypes.values().contains(className)) {
            for (String wrapper : primitiveTypes.keySet()) {
                if (primitiveTypes.get(wrapper).equals(className)) {
                    return wrapper;
                }
            }
        }
        return null;
    }

    public static Method getGetter(Class<?> clazz, String property) {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (isGetter(method) && property.equals(getProperty(method, false))) {
                return method;
            }
        }
        return null;
    }

    public static Method getSetter(Class<?> clazz, String property) {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (isSetter(method) && property.equals(getProperty(method, true))) {
                return method;
            }
        }
        return null;
    }
    
    public static Object executeGetter(Object obj, String property) throws DatandroidReflectionException {
        Method method = ReflectionUtil.getGetter(obj.getClass(), property);
        Object value = null;
        if (method != null) {
            try {
                value = method.invoke(obj, new Object[0]);
            } catch (Exception e) {
                throw new DatandroidReflectionException(e);
            }
        }
        return value;
    }
    
    public static void executeSetter(Object obj, String property, Object value) throws DatandroidReflectionException {
        Method method = ReflectionUtil.getSetter(obj.getClass(), property);        
        if (method != null) {
            Object newValue = castNumber(value, method.getParameterTypes()[0]);
            try {
                method.invoke(obj, newValue);                
            } catch (Exception e) {
                throw new DatandroidReflectionException(e);
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T castNumber(Object value, Class<T> resultClass) throws DatandroidReflectionException {
        if (value == null) {
            return null;
        }
        if (resultClass.isAssignableFrom(value.getClass())) {
            return (T) value;
        }
        // Si estamos aquí es porque no podemos devolver el valor como tal
        String resultClassName = resultClass.getName();
        if (numbers.containsValue(resultClassName)) {
            // El argumento es un tipo primitivo
            for (String className : numbers.keySet()) {
                if (numbers.get(className).equals(resultClassName)) {
                    resultClassName = className;
                    break;
                }
            }
        }
        // Hay que transformar value en la clase del argumento
        if (numbers.containsKey(value.getClass().getName()) && 
                numbers.containsKey(resultClassName)) {
            String methodName = numbers.get(resultClassName) + "Value";
            try {
                Method m = value.getClass().getDeclaredMethod(methodName, new Class[0]);
                return (T) m.invoke(value, new Object[0]);
            } catch(Exception e) {
                throw new DatandroidReflectionException(e);
            }
        } else {
            throw new DatandroidReflectionException("ClassCastException: " + value.getClass() 
                        + " to " + resultClassName);
        }
        
    }
    
    public static Class<?> getClassForName(String className) throws DatandroidReflectionException {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new DatandroidReflectionException(e);
        }
    }

    public static Map<String, Class<?>> classAttributes(Class<?> clazz) {
        Map<String, Class<?>> properties = new HashMap<String, Class<?>>();
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            final String property = getProperty(method, true);
            if (property != null) {
                properties.put(property, method.getParameterTypes()[0]);
            }
        }
        return properties;
    }

    /**
     * Método de utilidad de devuelve el nombre de la propiedad a partir de un
     * método
     * 
     * @param method
     *            objeto que representa un método
     * @return nombre de la propiedad o null si no se trata de un método getter
     */
    private static String getProperty(final Method method, boolean setter) {
        String propertyName = null;

        if ((setter && isSetter(method)) || (!setter && isGetter(method))) {
            propertyName = method.getName();

            int position = propertyPosition(propertyName, setter);

            propertyName = propertyName.substring(position);
            final char first = propertyName.charAt(0);
            if (first >= 'A' || first <= 'Z') {
                propertyName = Character.toLowerCase(first)
                        + propertyName.substring(1);
            }
        }
        return propertyName;
    }

    /**
     * Método de utilidad que devuelve un booleano si el método es de tipo
     * setter
     * 
     * @param method
     *            objeto que representa un método
     * @return true si es un método de tipo setter y falso en cualquier otro
     *         caso
     */
    private static boolean isSetter(final Method method) {
        boolean isSetter = true;
        final String methodName = method.getName();

        if (method.getParameterTypes().length != 1) {
            isSetter = false;
        }

        if (isSetter && !method.getReturnType().getName().equals("void")) {
            isSetter = false;
        }

        if (isSetter) {
            int position = propertyPosition(methodName, true);

            if (position == 0 || methodName.length() == position) {
                isSetter = false;
            }
        }
        return isSetter;
    }

    /**
     * Método de utilidad que devuelve un booleano si el método es de tipo
     * getter
     * 
     * @param method
     *            objeto que representa un método
     * @return true si es un método de tipo getter y falso en cualquier otro
     *         caso
     */
    private static boolean isGetter(final Method method) {
        boolean isGetter = true;
        final String methodName = method.getName();

        if (method.getParameterTypes().length > 0) {
            isGetter = false;
        }

        if (isGetter && method.getReturnType().getName().equals("void")) {
            isGetter = false;
        }

        if (isGetter) {
            int position = propertyPosition(methodName, false);

            if (position == 0 || methodName.length() == position) {
                isGetter = false;
            }
        }
        return isGetter;
    }

    /**
     * Método de utilidad que a partir del nombre de un método devieñve la
     * posición en la que empieza el nombre de la propiedad
     * 
     * @param methodName
     *            nombre del método
     * @param setter
     *            indica si se busca un setter o un getter
     * @return posición, dentro del nombre del método, en el que empieza el
     *         nombre de la propiedad o 0 si no es un método que empiece por
     *         'get' o 'is' si es getter o 'set' si es setter
     */
    private static int propertyPosition(final String methodName, boolean setter) {
        int position = 0;
        if (setter) {
            if (methodName.startsWith("set")) {
                position = "set".length();
            }
        } else {
            for (String preffix : getterPreffix) {
                if (methodName.startsWith(preffix)) {
                    position = preffix.length();
                }
            }
        }
        return position;
    }

}
